/**
	Copyright (C) 2009  Tobias Domhan

    This file is part of AndOpenGLCam.

    AndObjViewer is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    AndObjViewer is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with AndObjViewer.  If not, see <http://www.gnu.org/licenses/>.

 */
package com.artollkitplus.benchmark;

import java.nio.ByteBuffer;
import java.util.List;

import javax.microedition.khronos.opengles.GL10;

import com.artollkitplus.benchmark.R;

import android.content.res.Resources;
import android.graphics.PixelFormat;
import android.hardware.Camera;
import android.hardware.Camera.Parameters;
import android.hardware.Camera.PreviewCallback;
import android.hardware.Camera.Size;
import android.opengl.GLSurfaceView;
import android.util.Log;

/**
 * Handles callbacks of the camera preview
 * camera preview demo:
 * http://developer.android.com/guide/samples/ApiDemos/src/com/example/android/apis/graphics/CameraPreview.html
 * YCbCr 420 colorspace infos:
	 * http://wiki.multimedia.cx/index.php?title=YCbCr_4:2:0
	 * http://de.wikipedia.org/wiki/YCbCr-Farbmodell
	 * http://www.elektroniknet.de/home/bauelemente/embedded-video/grundlagen-der-videotechnik-ii-farbraum-gammakorrektur-digitale-video-signale/4/
 * @see android.hardware.Camera.PreviewCallback
 * @author Tobias Domhan
 */
public class CameraPreviewHandler implements PreviewCallback {
	private GLSurfaceView glSurfaceView;
	private OpenGLCamRenderer renderer;
	private Resources res;
	private int textureSize=256;
	private int previewFrameWidth=240;
	private int previewFrameHeight=160;
	private int bwSize=previewFrameWidth*previewFrameHeight;//size of the black/white image
	private int mode = Config.MODE_IMAGE_RGB;
	private Object modeLock = new Object();
	private MarkerInfo markerInfo;
	private ConversionWorker convWorker;
	public byte[] frame;
	private static long	_last_fps_tick;	//temp debug
	
	public CameraPreviewHandler(GLSurfaceView glSurfaceView,
			OpenGLCamRenderer rend, Resources res, MarkerInfo markerInfo) {
		this.glSurfaceView = glSurfaceView;
		this.renderer = rend;
		this.res = res;
		this.markerInfo = markerInfo;
		convWorker = new ConversionWorker(rend);
	}
	
	static { 
	    //System.loadLibrary( "imageprocessing" );
	    System.loadLibrary( "yuv420sp2rgb" );	
	} 
	
	private native void yuv420sp2rgb(byte[] in, int width, int height, int textureSize, byte[] out);

	//private native void binarize(byte[] in, int width, int height, byte[] out, int threshold);
	
	//private native void detect_edges(byte[] in, int width, int height, byte[] out, int threshold);

	//private native void detect_edges_simple(byte[] in, int width, int height, byte[] out, int threshold);
	
	/**
	 * the size of the camera preview frame is dynamic
	 * we will calculate the next power of two texture size
	 * in which the preview frame will fit
	 * and set the corresponding size in the renderer
	 * how to decode camera YUV to RGB for opengl:
	 * http://groups.google.de/group/android-developers/browse_thread/thread/c85e829ab209ceea/d3b29d3ddc8abf9b?lnk=gst&q=YUV+420#d3b29d3ddc8abf9b
	 * @param camera
	 */
	public void init(Camera camera) throws Exception {
		Log.i("CameraPreviewHandler", "init(Camera camera)");
		Parameters camParams = camera.getParameters();
		//check if the pixel format is supported
		if (camParams.getPreviewFormat() != PixelFormat.YCbCr_420_SP) {
			//semi-planar YCbCr 4:2:2 : two arrays, one with all Ys, one with Cb and Cr. 
			//Quelle: http://www.celinuxforum.org/CelfPubWiki/AudioVideoGraphicsSpec_R2
			throw new Exception(res.getString(R.string.error_unkown_pixel_format));
		}			
		Size previewSize = camParams.getPreviewSize();
		previewFrameWidth = previewSize.width;
		previewFrameHeight = previewSize.height;
		textureSize = (int) Math.pow(2, Math.ceil(Math.log((double)Math.max(previewFrameWidth, previewFrameHeight))/Math.log(2)));
		CurrentConfig.DEFAULT_IMAGE_WIDTH = previewSize.width;
		CurrentConfig.DEFAULT_IMAGE_HEIGT = previewSize.height;
		bwSize = previewFrameWidth * previewFrameHeight;
		frame = new byte[bwSize*3];
		for (int i = 0; i < frame.length; i++) {
			frame[i]=(byte) 128;
		}
		renderer.setPreviewFrameSize(textureSize, previewFrameWidth, previewFrameHeight);
		markerInfo.setImageSize(previewFrameWidth, previewFrameHeight);
	}

	/**
	 * new frame from the camera arrived. convert and hand over
	 * to the renderer
	 * how to convert between YUV and RGB:http://en.wikipedia.org/wiki/YUV#Y.27UV444
	 * Conversion in C-Code(Android Project):
	 * http://www.netmite.com/android/mydroid/donut/development/tools/yuv420sp2rgb/yuv420sp2rgb.c
	 * http://code.google.com/p/android/issues/detail?id=823
	 * @see android.hardware.Camera.PreviewCallback#onPreviewFrame(byte[], android.hardware.Camera)
	 */
	//@Override
	public void onPreviewFrame(byte[] data, Camera camera) {
		if(CurrentConfig.USE_MENU_PAUSE_SCREEN){
			if(!CurrentConfig.MENU_PAUSE_SCREEN_ON){
				if (data == null)
					return;
				convWorker.nextFrame(data);
				markerInfo.detectMarkers(data);
			}
		}else{
			if (data == null)
				return;
			convWorker.nextFrame(data);
			markerInfo.detectMarkers(data);
		}
		if (System.currentTimeMillis() - _last_fps_tick > 5000)//temp debug
		{
			_last_fps_tick = System.currentTimeMillis();
			Log.e("CameraPreviewHandler","onPreviewFrame() data.length "+data.length);
		}
	}

	protected void setMode(int pMode) {
		synchronized (modeLock) {
			this.mode = pMode;
			switch(mode) {
			case Config.MODE_IMAGE_RGB:
				renderer.setMode(GL10.GL_RGB);
				break;
			case Config.MODE_IMAGE_GRAY:
				renderer.setMode(GL10.GL_LUMINANCE);
				break;
//			case Config.MODE_IMAGE_BIN:
//				renderer.setMode(GL10.GL_LUMINANCE);
//				break;
//			case Config.MODE_IMAGE_EDGE:
//				renderer.setMode(GL10.GL_LUMINANCE);
//				break;
//			case Config.MODE_IMAGE_CONTOUR:
//				renderer.setMode(GL10.GL_LUMINANCE);
//				break;
			}
		}		
	}
	
	/**
	 * A worker thread that does colorspace conversion in the background.
	 * Need so that we can throw frames away if we can't handle the throughput.
	 * Otherwise the more and more frames would be enqueued, if the conversion did take
	 * too long.
	 * @author Tobias Domhan
	 */
	class ConversionWorker extends Thread {
		private byte[] curFrame;
		private OpenGLCamRenderer render;

		public ConversionWorker(OpenGLCamRenderer rend) {
			setDaemon(true);
			this.render = rend;
			start();
		}
		@Override
		public synchronized void run() {			
			try {
				wait();//wait for initial frame
			} catch (InterruptedException e) {}
			while(true) {
				render.getFrameLock().lock();
				synchronized (modeLock) {
					switch(mode) {
					case Config.MODE_IMAGE_RGB:
						yuv420sp2rgb(curFrame, previewFrameWidth, previewFrameHeight, textureSize, frame);   
						render.setNextFrame(ByteBuffer.wrap(frame));
						break;
					case Config.MODE_IMAGE_GRAY:
						//we will copy the array, assigning a new reference will cause multihreading issues
						//frame = curFrame;//WILL CAUSE PROBLEMS, WHEN SWITCHING BACK TO RGB
						//System.arraycopy(curFrame, 0, frame, 0, bwSize);
						render.setNextFrame(ByteBuffer.wrap(curFrame));	//looks like it's working
						break;
//					case Config.MODE_IMAGE_BIN:
//						binarize(curFrame, previewFrameWidth, previewFrameHeight, frame, 100);
//						render.setNextFrame(ByteBuffer.wrap(frame));
//						break;
//					case Config.MODE_IMAGE_EDGE:
//						detect_edges(curFrame, previewFrameWidth, previewFrameHeight, frame,20);
//						render.setNextFrame(ByteBuffer.wrap(frame));
//						break;
//					case Config.MODE_IMAGE_CONTOUR:
//						detect_edges_simple(curFrame, previewFrameWidth, previewFrameHeight, frame,150);
//						render.setNextFrame(ByteBuffer.wrap(frame));
//						break;
					}
				}
				render.getFrameLock().unlock();
				glSurfaceView.requestRender();
				try {
					wait();
				} catch (InterruptedException e) {}
			}
		}
		synchronized void nextFrame(byte[] frame) {
			if(this.getState() == Thread.State.WAITING) {
				curFrame = frame;
				this.notify();
			} 
		}
	}
	
	public static Size getOptimalPreviewSize(List<Size> sizes, int w, int h) {
	    final double ASPECT_TOLERANCE = 0.1;
	    final double MAX_DOWNSIZE = 1.5;

	    double targetRatio = (double) w / h;
	    if (sizes == null) return null;

	    Size optimalSize = null;
	    double minDiff = Double.MAX_VALUE;

	    int targetHeight = h;

	    // Try to find an size match aspect ratio and size
	    for (Size size : sizes) {
	        double ratio = (double) size.width / size.height;
	        double downsize = (double) size.width / w;
	        if (downsize > MAX_DOWNSIZE) {
	            //if the preview is a lot larger than our display surface ignore it
	            //reason - on some phones there is not enough heap available to show the larger preview sizes 
	            continue;
	        }
	        if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) continue;
	        if (Math.abs(size.height - targetHeight) < minDiff) {
	            optimalSize = size;
	            minDiff = Math.abs(size.height - targetHeight);
	        }
	    }

	    // Cannot find the one match the aspect ratio, ignore the requirement
	    //keep the max_downsize requirement
	    if (optimalSize == null) {
	        minDiff = Double.MAX_VALUE;
	        for (Size size : sizes) {
	            double downsize = (double) size.width / w;
	            if (downsize > MAX_DOWNSIZE) {
	                continue;
	            }
	            if (Math.abs(size.height - targetHeight) < minDiff) {
	                optimalSize = size;
	                minDiff = Math.abs(size.height - targetHeight);
	            }
	        }
	    }
	    //everything else failed, just take the closest match
	    if (optimalSize == null) {
	        minDiff = Double.MAX_VALUE;
	        for (Size size : sizes) {
	            if (Math.abs(size.height - targetHeight) < minDiff) {
	                optimalSize = size;
	                minDiff = Math.abs(size.height - targetHeight);
	            }
	        }
	    }

	    return optimalSize;
	}


}
